package fr.grassaud.hmvc.web.session;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.grassaud.hmvc.session.HMVCSessionFactory;
import fr.grassaud.hmvc.session.TriadListener;

public class HMVCSessionFactoryLoader {

    public static final String HMVC_FACTORY = "HMVC_FACTORY";

    public static final String ROOT_CONTROLLER_CLASS_PARAM = "rootClass";

    public static final String TRIAD_LISTENER_CLASS_PARAM = "listenersClass";

    public static final String ROOT_HMVC_FACTORY_ATTRIBUTE = "HMVC_FACTORY_ATTRIBUTE";

    private Logger logger = LoggerFactory.getLogger(HMVCSessionFactoryLoader.class);

    /**
     * Map from (thread context) ClassLoader to corresponding 'current'
     * HMVCSessionFactory.
     */
    private static final Map<ClassLoader, HMVCSessionFactory> currentFactoryPerThread = new ConcurrentHashMap<ClassLoader, HMVCSessionFactory>(
	    1);

    /**
     * The 'current' HMVC Session factory, if the HMVCSessionFactoryLoader class
     * is deployed in the web app ClassLoader itself.
     */
    private static volatile HMVCSessionFactory currentFactory;

    /**
     * The root hmvc session factory instance that this loader manages.
     */
    private HMVCSessionFactory factory;

    /**
     * Initialize HMVC Factroy application context for the given servlet
     * context, using the application context provided at construction time, or
     * creating a new one according to the "{@link #ROOT_CONTROLLER_CLASS_PARAM
     * contextClass}" and context-params.
     * 
     * @param servletContext
     *            current servlet context
     * @return the new HMVCSessionFactory
     */
    public HMVCSessionFactory initWebApplicationContext(ServletContext servletContext) {
	if (servletContext.getAttribute(ROOT_HMVC_FACTORY_ATTRIBUTE) != null) {
	    throw new IllegalStateException(
		    "Cannot initialize HMVC factory because there is already a root factory context present - "
			    + "check whether you have multiple HMVCFactoryLoader* definitions in your web.xml!");
	}

	servletContext.log("Initializing Spring root WebApplicationContext");
	logger.info("Root HMVCSSessionFactory : initialization started");

	try {
	    // Store factory in local instance variable, to guarantee that
	    // it is available on ServletContext shutdown.
	    if (this.factory == null) {
		this.factory = createSessionFactory(servletContext);

	    }
	    initTriadListeners(this.factory, servletContext);

	    servletContext.setAttribute(ROOT_HMVC_FACTORY_ATTRIBUTE, this.factory);

	    ClassLoader ccl = Thread.currentThread().getContextClassLoader();
	    if (ccl == HMVCSessionFactoryLoader.class.getClassLoader()) {
		currentFactory = this.factory;
	    } else if (ccl != null) {
		currentFactoryPerThread.put(ccl, this.factory);
	    }

	    return this.factory;
	} catch (RuntimeException ex) {
	    logger.error("Factory initialization failed", ex);
	    servletContext.setAttribute(ROOT_HMVC_FACTORY_ATTRIBUTE, ex);
	    throw ex;
	}

    }

    protected HMVCSessionFactory createSessionFactory(ServletContext ctx) {
	HMVCSessionFactory result = null;
	result = new HMVCSessionFactory();
	String controllerClass = ctx.getInitParameter(ROOT_CONTROLLER_CLASS_PARAM);
	try {
	    Class<?> rootClass = Class.forName(controllerClass);
	    result.setRootControllerClass(rootClass);
	} catch (ClassNotFoundException e) {
	    result = null;
	    logger.error("ERROR", e);
	}

	return result;
    }

    public void closeHMVCSessionFactory(ServletContext servletContext) {

	try {
	    servletContext.log("Closing HMVC root application");
	} finally {
	    ClassLoader ccl = Thread.currentThread().getContextClassLoader();
	    if (ccl == HMVCSessionFactoryLoader.class.getClassLoader()) {
		currentFactory = null;
	    } else if (ccl != null) {
		currentFactoryPerThread.remove(ccl);
	    }
	    servletContext.removeAttribute(ROOT_HMVC_FACTORY_ATTRIBUTE);

	}
    }

    protected void initTriadListeners(HMVCSessionFactory factory, ServletContext ctx) {
	String listenerParam = ctx.getInitParameter(TRIAD_LISTENER_CLASS_PARAM);
	String[] listeners = listenerParam.split(",");
	for (String listenerClass : listeners) {
	    listenerClass = listenerClass.trim();
	    try {
		Class<?> listener = Class.forName(listenerClass);
		if (TriadListener.class.isAssignableFrom(listener)) {
		    TriadListener listenerCreated = createListener(listener, ctx);
		    if (listenerCreated != null) {
			factory.addTriadListener(listenerCreated);
		    } else {
			logger.warn(
				"Cannot find a constructor with no argument or using ServletContext for listener {}",
				listener);
		    }
		}
	    } catch (ClassNotFoundException e) {
		logger.error("ERROR", e);
	    }
	}

    }

    /**
     * Creates the given listener to add to the <code>HMVCSessionFactory</code>
     * @param listener the class of listener to instantiate
     * @param ctx the ServletContext to use 
     * @return an instance of <code>TriadListener</code>
     */
    private TriadListener createListener(Class<?> listener, ServletContext ctx) {
	TriadListener result = null;
	for (Constructor<?> con : listener.getDeclaredConstructors()) {
	    Class<?>[] parametersType = con.getParameterTypes();
	    if (parametersType.length > 0) {
		for (Class<?> argType : parametersType) {
		    if (argType.isAssignableFrom(ServletContext.class)) {
			result = instantiateListener(con, ctx);
			break;
		    }
		}
	    } else {
		result = instantiateListener(con, null);
		break;
	    }
	}
	return result;
    }

    
    /**
     * Instantiates a <code>TriadListener</code> from the given constructor
     * @param con the constructor of the listener to instantiate
     * @param ctx the servlet context used
     * @return the instance of the listener, <code>null</code> if there is a problem
     */
    private TriadListener instantiateListener(Constructor<?> con, ServletContext ctx) {

	Object[] params = null;
	if (ctx != null) {
	    params = new Object[] { ctx };
	}
	TriadListener result = null;
	try {
	    result = (TriadListener) con.newInstance(params);
	    logger.info("HMVS Session listener created : {} ", result.getClass());
	} catch (InstantiationException e) {
	    logger.error("ERROR", e);
	} catch (IllegalAccessException e) {
	    logger.error("ERROR", e);
	} catch (IllegalArgumentException e) {
	    logger.error("ERROR", e);
	} catch (InvocationTargetException e) {
	    logger.error("ERROR", e);
	}
	return result;
    }

    /**
     * Obtain the HMVC session factory for the current thread (i.e. for the
     * current thread's context ClassLoader, which needs to be the web
     * application's ClassLoader).
     * 
     * @return the current HMVC session factory, or <code>null</code> if none
     *         found
     */
    public static HMVCSessionFactory getCurrentHMVCSessionFactory() {
	ClassLoader ccl = Thread.currentThread().getContextClassLoader();
	if (ccl != null) {
	    HMVCSessionFactory ccpt = currentFactoryPerThread.get(ccl);
	    if (ccpt != null) {
		return ccpt;
	    }
	}
	return currentFactory;
    }

}
